<?php
/*
* Pure: A dead simple and heavily customizable blogging engine
* Copyright (C) 2007 Ben Ng & Payam Yousefi
* 
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* 
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* 
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
* 
* VERSION: 0.2 EPSILON
*/

class pure {
	var $_usr=false; //This is filled out with user details and permissions by pure()
	var $_cfg=array(
		'table_path'=>'tables/',
		'prefix'=>'',
	);
	
	/*
	* This will attempt to restablish the current session
	*/
	function pure($prefix='',$using_geshi=false) {
		//Start session
		session_start();
		$this->_cfg['prefix']=$prefix;
		//Add prefix
		$this->_cfg['table_path']=$prefix.$this->_cfg['table_path'];
		//First off, set up table objects that we will need
		$table_status=array(
			'archives'=>$this->table_exists('archives'),
			'posts'=>$this->table_exists('posts'),
			'categories'=>$this->table_exists('categories'),
			'pages'=>$this->table_exists('pages'),
			'users'=>$this->table_exists('users'),
			'comments'=>$this->table_exists('comments'),
		);
		//And our geshi object if needed
		if($using_geshi)
			$this->geshi=new geshi('','php');
		//Mount the tables
		$this->mount_table('archives');
		$this->mount_table('posts');
		$this->mount_table('categories');
		$this->mount_table('pages');
		$this->mount_table('users');
		$this->mount_table('comments');
		//Autofix tables we detected were missing
		if($table_status['archives']==false) {
			$this->tb_archives->addKey('month');
			$this->tb_archives->addField('created');
			$this->tb_archives->commit();
		}
		if($table_status['posts']==false) {
			$this->tb_posts->addField('title');
			$this->tb_posts->addField('content');
			$this->tb_posts->addField('timestamp');
			$this->tb_posts->addField('month'); //Special format for faster by-month-year processing: date('mY')
			$this->tb_posts->addField('author');
			$this->tb_posts->addField('comments');
			$this->tb_posts->addField('comment_count');
			$this->tb_posts->addField('is_published');
			$this->tb_posts->addField('categories');
			$this->tb_posts->commit();
			
			//Make a test post
			$this->make_post('Post table created','This post confirms that your post table is working.
If you have just installed Pure, the default username and password are both "admin".');
		}
		if($table_status['categories']==false) {
			$this->tb_categories->addKey('name');
			$this->tb_categories->commit();
			
			//Add the Uncategorized category
			if(!$this->add_category('Uncategorized'))
				echo('Error: Could not create new category');
		}
		if($table_status['pages']==false) {
			$this->tb_pages->addField('title');
			$this->tb_pages->addField('content');
			$this->tb_pages->addField('timestamp');
			$this->tb_pages->addField('is_published');
			$this->tb_pages->commit();
		}
		if($table_status['users']==false) {
			$admin=array(
				'username'=>'admin',
				'hash'=>md5('admin'),
			);
			$this->tb_users->addField('username');
			$this->tb_users->addField('hash');
			$this->tb_users->addRow($admin);
			$this->tb_users->commit();
		}
		if($table_status['comments']==false) {
			$this->tb_comments->addField('timestamp');
			$this->tb_comments->addField('approved');
			$this->tb_comments->addField('prominent');
			$this->tb_comments->addField('parent');
			$this->tb_comments->addField('content');
			$this->tb_comments->addField('ip');
			$this->tb_comments->addField('name');
			$this->tb_comments->addField('email');
			$this->tb_comments->commit();
		}
		//All done! Ready to begin work.
	}
	
	/*
	* Attempt to mount or create a table for use
	*/
	function mount_table($name) {
		$valid_name=preg_replace('[^0-9a-zA-Z_]','',$name);
		
		//Check if this is a valid table name
		if($valid_name!==$name || strlen($valid_name)==0) {
			trigger_error('Invalid table name "'.$name.'".');
			return false;
		}
		
		$tbname='tb_'.$name;
		$filename=$this->_cfg['table_path'].$tbname.'.php';
		
		//Check if this table has not been mounted
		if(!isset($this->$tbname)) {
			//Check if this table file exists
			if($this->table_exists($name)) {
				//Mount the table
				$this->$tbname=new fetch($filename);
				return true;
			}
			//If not, automagically create the table
			else {
				$this->$tbname=new fetch($filename,true);
				return true;
			}
		}
		//If not we can't mount it again
		else {
			trigger_error('The table "'.$name.'" is already mounted.');
			return false;
		}
	}
	
	/*
	* Is the user logged in?
	*/
	function is_logged_in() {
		$this->log_in_cookie();
		return (array_key_exists('auth_id',$_SESSION)&&is_int($_SESSION['auth_id']) && $this->tb_users->idExists($_SESSION['auth_id']));
	}
	
	/*
	* Log this user in
	*/
	function log_in($uid,$pass) {
		//If uid recieved was a username, attempt to convert to a uid
		if(is_string($uid)) {
			$uid=$this->usernameToId($uid);
		}
		$hash=md5($pass);
		return $this->log_in_hash($uid,$hash);
	}
	
	/*
	* Log in with cookie information
	*/
	function log_in_cookie(){
		if(isset($_COOKIE["ui"])){
			$cookies = explode("-", $_COOKIE["ui"]);
			$userid = $cookies[0];
			$mdpass = $cookies[1];
			$userid+=0;
			return $this->log_in_hash($userid, $mdpass);
		}
		return false;
	}
	
	function log_in_hash($uid, $hash) {
		//is_int() makes sure that the either user id is an int or the user is also already logged in
		//If already logged in, move on. Or else, check if passwords match
		if(is_int($uid) && $this->tb_users->getFieldValue($uid,'hash')==$hash) {
			//Update _usr with proper information
			$this->_usr=array(
				'username'=>$this->tb_users->getFieldValue($uid,'username'),
				'hash'=>$this->tb_users->getFieldValue($uid,'hash'),
			);
			$_SESSION['auth_id']=$uid;
			//Now we update the last active time
			$this->tb_users->updateRow($uid,array('last_active'=>time()));
			$this->tb_users->commit();
			return true;
		}
		return false;
	}
	
	/*
	* Rebuild the rss XML file
	*/
	function rebuildRSS() {
		//Get all posts
		$posts=$this->loadposts_all();
		
		//Remove the last# posts
		$i=0;
		$limit=count($posts)-RSS_MAX_ENTRIES;
		foreach($posts as $postid=>$postdata) {
			if($i<$limit)
				unset($posts[$postid]);
			$i++;
		}
		
		//Reverse the array
		krsort($posts);
		
		//Build the file
		$file='<?xml version="1.0"?>
<rss version="2.0">
	<channel>
		<title>'.htmlentities(SITE_TITLE,ENT_QUOTES,'UTF-8').'</title>
		<link>'.htmlentities(SITE_URL,ENT_QUOTES,'UTF-8').'</link>
		<description>'.htmlentities(META_DESC,ENT_QUOTES,'UTF-8').'</description>
		<language>en-us</language>
		
		<copyright>Copyright '.date('Y').' '.SITE_URL.'</copyright>
		<lastBuildDate>'.htmlentities(date('D, d M Y H:i:s O'),ENT_QUOTES,'UTF-8').'</lastBuildDate>

		<generator>'.htmlentities(PURE_VERSION,ENT_QUOTES,'UTF-8').'</generator>
		<managingEditor>'.htmlentities(SITE_EMAIL,ENT_QUOTES,'UTF-8').'</managingEditor>
		<webMaster>'.htmlentities(SITE_EMAIL,ENT_QUOTES,'UTF-8').'</webMaster>
		<ttl>'.htmlentities(RSS_TTL,ENT_QUOTES,'UTF-8').'</ttl>';
		
		if(is_array($posts)&&count($posts)>0) {
			//Add the posts
			foreach($posts as $postID=>$postData) {
				//Start the item
				$post='
				<item>
					<author>'.htmlentities($this->tb_users->getFieldValue($postData['author'],'username'),ENT_QUOTES,'UTF-8').'</author>
					<comments>'.htmlentities(SITE_URL.'/index.php?post='.$postID,ENT_QUOTES,'UTF-8').'</comments>
					<link>'.htmlentities(SITE_URL.'/index.php?post='.$postID,ENT_QUOTES,'UTF-8').'</link>
					<quid>'.htmlentities($postID,ENT_QUOTES,'UTF-8').'</quid>
					<pubDate>'.htmlentities(date('D, d M Y H:i:s O',$postData['timestamp']),ENT_QUOTES,'UTF-8').'</pubDate>
					<title>'.htmlentities($postData['title'],ENT_QUOTES,'UTF-8').'</title>';
				
				//Control excerpt length if too big
				$postContent=$postData['content'];
				if(strlen($postContent)>RSS_EXCERPT_LENGTH)
					$postContent=substr($postContent,0,RSS_EXCERPT_LENGTH-3).'...';
				
				//Barse bbCode?
				if(RSS_PARSE_BBCODE)
					$postContent=$this->bbcode($postContent,true);
				
				//Clean up
				$postContent=htmlentities($postContent,ENT_QUOTES,'UTF-8');
				
				//Add in the content
				$post.='
					<description>
					'.$postContent.'
					</description>
					';
				
				//End the item
				$post.='
				</item>';
				
				$file.=$post;
			}
		}
		
		//Add the end
		$file.='
	</channel>
</rss>';
		return $file;
	}
	
	/*
	* Turn a username to an id
	*/
	function usernameToId($username) {
		return $this->tb_users->firstMatchingId('strtolower(%username%)==strtolower(\''.fetch::qEscape($username).'\')');
	}
	
	/*
	* Does a table exist?
	*/
	function table_exists($name) {
		return file_exists($this->_cfg['table_path'].'tb_'.$name.'.php');
	}
	
	/*
	* How many posts?
	*/
	function numOfPosts() {
		return count($this->tb_posts->select('true'));
	}
	
	/*
	* Does a post exist?
	*/
	function post_exists($post_id) {
		return count($this->tb_posts->select('%%ID%%==\''.fetch::qEscape($post_id).'\'',array()))===1;
	}
	
	/*
	* Does a page exist?
	*/
	function page_exists($page_id) {
		return count($this->tb_pages->select('%%ID%%==\''.fetch::qEscape($page_id).'\'',array()))===1;
	}
	
	/*
	* Loads posts made in the month of the year, defaulting to this year
	*/
	function loadposts_month($month,$year=false,$show_drafts=false,$fields=false) {
		if($month<1||$month>12)
			trigger_error('impossible month');
		if($year===false)
			$year=date('Y');
		$result=$this->tb_posts->select('%month%==\''.fetch::qEscape($month.$year).'\''.($show_drafts!==true?'&&%is_published%===true':''),$fields);
		if(count($result)==0)
			return false;
		else
			return $result;
	}
	
	/*
	* Loads all posts ever made
	*/
	function loadposts_all($show_drafts=false,$fields=false) {
		$result=$this->tb_posts->select('true'.($show_drafts!==true?'&&%is_published%===true':''),$fields);
		if(count($result)==0)
			return false;
		else
			return $result;
	}
	
	/*
	* Gets the list of categories
	*/
	function get_categories() {
		return fetch::sortSelection($this->tb_categories->select('true'),'name',SORT_ASC);
	}
	
	/*
	* Gets posts in a category
	*/
	function get_cat($id) {
		if(!is_numeric($id))
			return false;
		
		return fetch::sortSelection($this->tb_posts->select('array_search('.$id.',%categories%)!==false'),'timestamp',SORT_DESC);
	}
	
	/*
	* Returns a category's name from its ID
	*/
	function cat_name($id) {
		if($this->tb_categories->idExists($id)===false)
			return false;
		
		return $this->tb_categories->getFieldValue($id,'name');
	}
	
	/*
	* Adds a category
	*/
	function add_category($name) {
		if($this->category_exists($name))
			return false;
		
		$this->tb_categories->addRow(array('name'=>$name));
		$this->tb_categories->commit();
		return true;
	}
	
	/*
	* Deletes a category
	*/
	function delete_category($id) {
		//Get rid of post references
		$postsInCat=$this->get_cat($id);
		
		//Get rid of listing
		if(!$this->tb_categories->idExists($id))
			return false;
		
		$this->tb_categories->dropRow($id);
		$this->tb_categories->commit();
		return true;
	}
	
	/*
	* Checks if a category exists
	*/
	function category_exists($name) {
		return $this->tb_categories->firstMatchingId('%name%==\''.fetch::qEscape($name).'\'')!==false;
	}
	
	/*
	* Checks if a category ID exists
	*/
	function category_id_exists($id) {
		return $this->tb_categories->idExists($id);
	}
	
	/*
	* Makes a post
	*/
	function make_post($title, $content, $comments=true, $publish=true, $categories=array(0)) {
		//Add an archive entry if needed
		$archive_id=$this->tb_archives->firstMatchingId('%month%==\''.date('mY').'\'');
		if($archive_id===false) {
			$archive_month=array(
				'month'=>date('mY'),
				'created'=>time(),
			);
			$archive_id=$this->tb_archives->addRow($archive_month);
		}
		//Commit the changes
		$this->tb_archives->commit();
		
		//Grab categories that exist
		$cats=array();
		if(is_array($categories))
			foreach($categories as $catID)
				if($this->tb_categories->idExists($catID))
					$cats[]=$catID;
		else
			$cats[]=0; //0 for uncategorized
		
		//Add the post
		$post=array(
			'title'=>$title,
			'content'=>$content,
			'timestamp'=>time(),
			'comment_count'=>0,
			'month'=>date('mY'),
			'author'=>$_SESSION['auth_id'],
			'comments'=>($comments===true?true:false),
			'is_published'=>($publish===true?true:false),
			'categories'=>$cats,
		);
		$post_id=$this->tb_posts->addRow($post);
		$this->tb_posts->commit();
		return $post_id;
	}
	
	/*
	* Opens a post
	*/
	function open_post($post_id) {
		if(!is_int($post_id))
			return false;
		$result=$this->tb_posts->select('%%ID%%=='.$post_id);
		if(count($result)==0)
			return false;
		else
			return array_pop($result);
	}
	
	/*
	* Edits a post
	*/
	function edit_post($post_id, $title, $content, $comments=true, $publish=false, $categories=false) {
		$id=$this->tb_posts->firstMatchingId('%%ID%%=='.$post_id);
		if($id===false)
			trigger_error('The post to be edited does not exist');
		if($categories==false)
			$categories=array(0);
		$update=array(
			'title'=>$title,
			'content'=>$content,
			'comments'=>($comments===true?true:false),
			'is_published'=>$publish,
			'categories'=>$categories,
		);
		$this->tb_posts->updateRow($id,$update);
		$this->tb_posts->commit();
		return true;
	}
	
	/*
	* Deletes a post
	*/
	function delete_post($post_id) {
		$id=$this->tb_posts->firstMatchingId('%%ID%%=='.$post_id);
		$month=$this->tb_posts->getFieldValue($id,'month');
		if($id===false)
			trigger_error('The post to be deleted does not exist');
		$this->tb_posts->dropRow($id);
		$this->tb_posts->commit();
		//Now check if we need to delete the archive entry
		if($this->tb_posts->firstMatchingId('%month%=='.$month)===false) {
			$archive_id=$this->tb_archives->firstMatchingId('%month%=='.$month);
			$this->tb_archives->dropRow($archive_id);
			$this->tb_archives->commit();
		}
		return true;
	}
	
	/*
	* Makes a page
	*/
	function make_page($title, $content, $publish=true) {
		//Add the page
		$page=array(
			'title'=>$title,
			'content'=>$content,
			'timestamp'=>time(),
			'is_published'=>($publish===true?true:false),
		);
		$page_id=$this->tb_pages->addRow($page);
		$this->tb_pages->commit();
		return $page_id;
	}
	
	/*
	* Edits a page
	*/
	function edit_page($page_id, $title, $content, $publish=false) {
		$id=$this->tb_pages->firstMatchingId('%%ID%%=='.$page_id);
		if($id===false)
			trigger_error('The page to be edited does not exist');
		$update=array(
			'title'=>$title,
			'content'=>$content,
			'is_published'=>$publish,
		);
		$this->tb_pages->updateRow($id,$update);
		$this->tb_pages->commit();
		return true;
	}
	
	/*
	* Deletes a page
	*/
	function delete_page($page_id) {
		$id=$this->tb_pages->firstMatchingId('%%ID%%=='.$page_id);
		if($id===false)
			trigger_error('The page to be deleted does not exist');
		$this->tb_pages->dropRow($id);
		$this->tb_pages->commit();
		return true;
	}
	
	/*
	* Adds an attachment
	*/
	function attach($fileName,$tempFile,$uploadPath) {
		$realuploadPath=$uploadPath;
		if(!is_dir($realuploadPath)) {
			echo('<br />The upload path "'.htmlentities($realuploadPath,ENT_QUOTES,'UTF-8').'" does not exist');
			return false;
		}
		if($this->get_attachment($fileName,$uploadPath)!==false) {
			echo('<br />Error: Attachment filename already taken');
			return false;
		}
		if(filesize($tempFile)>MAX_UPLOAD_BYTES) {
			echo('<br />Error: Attachment too large');
			return false;
		}
		$uploadFile=$realuploadPath.'/'.$fileName;
		if(!move_uploaded_file($tempFile,$uploadFile)) {
			echo('<br />Error: Attachment did not upload properly');
			return false;
		}
		return $uploadFile;
	}
	
	/*
	* Gets a single attachment
	*/
	function get_attachment($fileName,$uploadPath) {
		$uploadPath=$uploadPath;
		$attachmentPath=$uploadPath.'/'.$fileName;
		if(!file_exists($attachmentPath))
			return false;
		$attachment['file_data']=file_get_contents($attachmentPath);
		$attachment['file_name']=$fileName;
		return $attachment;
	}
	
	/*
	* Gets a list of attachments
	*/
	function get_attachments($uploadPath) {
		$uploadPath=$uploadPath;
		$attachments=array();
		if($handle=opendir($uploadPath)) {
			while(false!==($file=readdir($handle)))
				if($file != "."&&$file != "..")
					$attachments[]=$file;
		}
		else
			return false;
		closedir($handle);
		return $attachments;
	}
	
	/*
	* Searches for attachments
	*/
	function search_attachments($searchText,$uploadPath) {
		$attachments=$this->get_attachments($uploadPath);
		$results=array();
		//If nothing to search for, return all attachemtns
		if($searchText=='')
			return $attachments;
		
		//If not, go on searching
		if(is_array($attachments)&&count($attachments)>0) {
			foreach($attachments as $attachmentName) {
				if(strpos(strtolower($attachmentName),strtolower($searchText))!==false)
					$results[]=$attachmentName;
			}
			sort($results);
			return $results;
		}
		else
			return false;
	}
	
	/*
	* Deletes an attachment
	*/
	function delete_attachment($fileName,$uploadPath) {
		$uploadPath=$uploadPath;
		$attachmentPath=$uploadPath.'/'.$fileName;
		if(!file_exists($attachmentPath))
			return false;
		return unlink($attachmentPath);
	}
	
	/*
	* Parses (a post) with full bbcode
	*/
	function parsePost($post,$geshi=false,$readmore=false) {
		//First, convert all readmores to a standard format
		$bb_find = array(
			'/\[readmore\](.*?)\[\/readmore\]/is',
			'/\[readmore\]/is',
		);
		$bb_replace = array(
			'[tempreadmore][url=index.php?post='.$readmore.']$1[/url][/tempreadmore]',
			'[tempreadmore][url=index.php?post='.$readmore.'][Read More][/url][/tempreadmore]',
		);
		$post=preg_replace($bb_find,$bb_replace,$post);
		
		//Deal with readmore if needed
		if($readmore!==false) {
			//Find the spot to cut the post at
			$cutpos=strpos($post,'[/tempreadmore]');
			if($cutpos!==false)
				$post=substr($post,0,$cutpos);
		}
		
		//Remove all temporary readmore tags
		$tempTags=array(
			'/\[tempreadmore\](.*?)\[\/tempreadmore\]/is',
			'/\[tempreadmore]/is',
			'/\[\/tempreadmore]/is',
		);
		$post=preg_replace($tempTags,array('','',''),$post);
		
		//Exclude normal [code] tags but generate bbcode outside them
		$post=str_replace(array('\\','\''),array('\\\\','\\\''),$post);
		//This mess will find [code] tags and enclose them in the proper html as well as GESHI function if necesscary
		$post='$parsed=$this->bbcode(htmlentities(\''.
			preg_replace(
			//Find code tags
			'/[\s]*\[code(=([a-zA-Z0-9_ ]*?))?\][\s]*([^\e]*?)[\s]*\[\/code\][\s]*/is',
			//Exclude from rest of post
			'\',ENT_QUOTES,"UTF-8")).
				//The code tag
				\'
				<code>
					<span class="codetitle">
						Code\'.
						
						//Is GESHI enabled, and can we highlight this language?
						($geshi&&$this->geshi->set_language(\'$2\')?
						
						//If yes, specify the language being used
						\'
						 ($2)
						</span>
						<br />
						<tt>\'.
							//Now highlight the code
							geshi_highlight(\'$3\',\'$2\',$this->_cfg[\'prefix\'].\'geshi/geshi/\',true).
						\'</tt>
						</code>\'
						
						:
						
						//If not, do not highlight the post
						\'
						</span>
						<br />
						<tt>\'.
							//Just replace with line breaks
							str_replace("\\n",\'<br />\',htmlentities(\'$3\',ENT_QUOTES,"UTF-8")).
						\'
						</tt>
						</code>\'
						).
						
						//The ending stuff
			$this->bbcode(htmlentities(\'',$post).
		'\',ENT_QUOTES,"UTF-8"));';
		//Process the post
		eval($post);
		return $parsed;
	}
	
	/*
	* Parses bbcode
	*/
	function bbcode($string,$rss=false) {
		$bb_find = array(
			'/\[attach\](.*?)\[\/attach\]/is',
			'/\[attach\=(.*?)\](.*?)\[\/attach\]/is',
			
			'/\[h1\](.*?)\[\/h1\]/is',
			'/\[h2\](.*?)\[\/h2\]/is',
			'/\[h3\](.*?)\[\/h3\]/is',
			
			'/\[hr]/',
			
			'/\[quote\](.*?)\[\/quote\]/is',
			
			'/\[b\](.*?)\[\/b\]/is',
			'/\[i\](.*?)\[\/i\]/is',
			'/\[u\](.*?)\[\/u\]/is',
			
			'/\[sub\](.*?)\[\/sub\]/is',
			'/\[sup\](.*?)\[\/sup\]/is',
			'/\[s\](.*?)\[\/s\]/is',
			
			'/\[url\=(.*?)\](.*?)\[\/url\]/is',
			
			'/\[post\=(.*?)\](.*?)\[\/post\]/is',
			'/\[page\=(.*?)\](.*?)\[\/page\]/is',
			
			'/\[url\](.*?)\[\/url\]/is',
			'/\[img\](.*?)\[\/img\]/is',
			
			'/\[right\](.*?)\[\/right\]/is',
			'/\[left\](.*?)\[\/left\]/is',
			'/\[clear\](.*?)\[\/clear\]/is',
			
			'/\n/',
		);
		
		$bb_replace = array(
			SITE_URL.'/download.php?attach=$1',
			'<a target="_blank" href="'.SITE_URL.'/download.php?attach=$1" class="content">$2</a>',
			
			'<h1>$1</h1',
			'<h2>$1</h2>',
			'<h3>$1</h3>',
			
			$rss?'<hr>':'<hr />',
			
			'<quote></quote>',
			
			'<strong>$1</strong>',
			'<em>$1</em>',
			'<u>$1</u>',
			
			'<sub>$1</sub>',
			'<sup>$1</sup>',
			'<s>$1</s>',
			
			'<a href="$1" class="content">$2</a>',
			
			'<a href="index.php?post=$1" class="content">$2</a>',
			'<a href="index.php?page=$1" class="content">$2</a>',
			
			'<a href="$1" class="content">$1</a>',
			$rss?'<img alt="user posted image" src="$1"></img>':
			'<img alt="user posted image" src="$1" />',
			
			$rss?'$1':'<span style="float:right;">$1</span>',
			$rss?'$1':'<span style="float:left;">$1</span>',
			$rss?'$1':'<span style="clear:both;">$1</span>',
			
			$rss?'<br>':'<br />',
		);
		
		return preg_replace($bb_find, $bb_replace,$string);

	}
	
	/*
	* Makes a comment
	*/
	function makeComment($target, $author, $comment, $email, $wordblacklist=array(), $ipblacklist=array()) {
		if($this->tb_posts->firstMatchingId('%%ID%%=='.$target)===false)
			die('Cannot comment to nonexistant post.');
		
		/*Get the last comment by this IP*/
		$commentsByIP=fetch::sortSelection($this->tb_comments->select('%ip%==\''.fetch::qEscape($_SERVER['REMOTE_ADDR']).'\'',array('timestamp')),'timestamp',SORT_ASC);
		if(count($commentsByIP)>0)
			$lastCmmnt=array_pop($commentsByIP);
		
		/*If the last comment was over x seconds*/
		if(count($commentsByIP)==0 || $this->is_logged_in() || $lastCmmnt['timestamp']+MINIMUM_COMMENT_INTERVAL<time()) {
			$approved=false;
			
			/*Check for previously approved comment by this email and IP address*/
			if(ALWAYS_AUTO_APPROVE_COMMENTS || $this->is_logged_in() ||
			(AUTO_APPROVE_COMMENTS && count($this->tb_comments->select('%email%==\''.fetch::qEscape($email).'\'&&%approved%&&%ip%==\''.fetch::qEscape($_SERVER['REMOTE_ADDR']).'\'',array()))>0)) {
				$approved=true;
			}
			
			/*Look for blacklisted IP*/
			if(count($ipblacklist)>0)
				foreach($ipblacklist as $badip)
					if(strpos($_SERVER['REMOTE_ADDR'],$badip)===0)
						if(IP_BLACKLIST_DENY)
							return NULL;
						else
							$approved=false;
			
			/*Look for blacklisted words*/
			$commentlower=strtolower($comment);
			if(count($wordblacklist)>0)
				foreach($wordblacklist as $badword)
					if(strpos($commentlower,strtolower($badword))!==false&&WORD_BLACKLIST_OVERRIDE)
						$approved=false;
			
			/*Add comment*/
			$this->tb_comments->addRow(
				array(
					'parent'=>$target,
					'name'=>$author,
					'content'=>$comment,
					'email'=>$email,
					'timestamp'=>time(),
					'ip'=>$_SERVER['REMOTE_ADDR'],
					'approved'=>$approved,
					'prominent'=>$this->is_logged_in()?$_SESSION['auth_id']:false,
				)
			);
			$this->tb_comments->commit();
			//Need to add to post count if automatically approved
			if($approved) {
				/*Increment post count*/
				$commentCount=$this->tb_posts->getFieldValue($target,'comment_count');
				$this->tb_posts->updateRow($target,array('comment_count'=>$commentCount+1));
				$this->tb_posts->commit();
			}
			return $approved;
		}
		return NULL;
	}
	
	/*
	* Approves a comment
	*/
	function approveComment($commentID) {
		//Check if comment exists
		if(!$this->tb_comments->idExists($commentID))
			trigger_error('Cannot approve nonexistant comment (ID "'.$commentID.'")');
		//Check if not already approved
		if(!$this->tb_comments->getFieldValue($commentID,'approved')) {
			//Approve comment
			$this->tb_comments->updateRow($commentID,array('approved'=>true));
			$this->tb_comments->commit();
			//Add to post comment count
			$parent=$this->tb_comments->getFieldValue($commentID,'parent');
			$this->tb_posts->updateRow($parent,array('comment_count'=>$this->tb_posts->getFieldValue($parent,'comment_count')+1));
			$this->tb_posts->commit();
		}
	}
	
	/*
	* Unapproves a comment
	*/
	function unapproveComment($commentID) {
		//Check if comment exists
		if(!$this->tb_comments->idExists($commentID))
			trigger_error('Cannot unapprove nonexistant comment (ID "'.$commentID.'")');
		//Check if already approved
		if($this->tb_comments->getFieldValue($commentID,'approved')) {
			//Unapprove comment
			$this->tb_comments->updateRow($commentID,array('approved'=>false));
			$this->tb_comments->commit();
			//Subtract from post comment count
			$parent=$this->tb_comments->getFieldValue($commentID,'parent');
			$this->tb_posts->updateRow($parent,array('comment_count'=>$this->tb_posts->getFieldValue($parent,'comment_count')-1));
			$this->tb_posts->commit();
		}
	}
	
	/*
	* Deletes a comment
	*/
	function deleteComment($commentID) {
		//Check if comment exists
		if(!$this->tb_comments->idExists($commentID))
			trigger_error('Cannot delete nonexistant comment (ID "'.$commentID.'")');
		//Subtract from post comment count only if comment was already approved
		if($this->tb_comments->getFieldValue($commentID,'approved')) {
			$parent=$this->tb_comments->getFieldValue($commentID,'parent');
			$this->tb_posts->updateRow($parent,array('comment_count'=>$this->tb_posts->getFieldValue($parent,'comment_count')-1));
			$this->tb_posts->commit();
		}
		//Delete comment
		$this->tb_comments->dropRow($commentID);
		$this->tb_comments->commit();
	}
	
	/*
	* Blacklists a comment (deletes all by same IP)
	*/
	function blacklistCommentIP($commentID) {
		//Get the IP
		$IP=$this->tb_comments->getFieldValue($commentID,'ip');
		//Get all comments by this IP
		$comments=$this->tb_comments->select('%ip%==\''.fetch::qEscape($IP).'\''.array());
		//Delete all posts by this IP
		foreach($comments as $cid=>$junk) {
			$this->tb_comments->dropRow($cid);
		}
		$this->tb_comments->commit();
		//Return deleted amount
		return count($comments);
	}

	/*
	* Creates a user
	*/
	function create_user($username, $password) {	
		$exists=$this->usernameToId($username);
		if($exists===false){
			$this->tb_users->addRow(array('username'=>$username,'hash'=>md5($password)));
			$this->tb_users->commit();
			return true;
		} else {			
			echo('<h2 class="red">Error</h2>
			A user with that name exists already. Please change.<br />');
			return false;
		}
	}
	
	/*
	* Edits a user
	*/
	function edit_user($user_id,$data) {
		if($this->tb_users->firstMatchingId('%%ID%%=='.$user_id)!==false) {
		$this->tb_users->updateRow($user_id,$data);
			$this->tb_users->commit();
			return true;
		}
		else
		 return false;
	}
	
	/*
	* Deletes a user
	*/
	function delete_user($user_id) {
		if($this->tb_users->firstMatchingId('%%ID%%=='.$user_id)!==false) {
			$this->tb_users->dropRow($user_id);
			$this->tb_users->commit();
			return true;
		}
		else
		 return false;
	}
	
	/*
	* Checks if an email address is valid
	* Used with permission from Cal Henderson
	* http://www.iamcal.com/publish/articles/php/parsing_email/
	*/
	function is_valid_email($email){
		$qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]';
		$dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]';
		$atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c'.
			'\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+';
		$quoted_pair = '\\x5c[\\x00-\\x7f]';
		$domain_literal = "\\x5b($dtext|$quoted_pair)*\\x5d";
		$quoted_string = "\\x22($qtext|$quoted_pair)*\\x22";
		$domain_ref = $atom;
		$sub_domain = "($domain_ref|$domain_literal)";
		$word = "($atom|$quoted_string)";
		$domain = "$sub_domain(\\x2e$sub_domain)*";
		$local_part = "$word(\\x2e$word)*";
		$addr_spec = "$local_part\\x40$domain";
		return preg_match("!^$addr_spec$!", $email) ? 1 : 0;
	}
}
?>